// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef SRC_STUB_INL_H_
#define SRC_STUB_INL_H_

#include "stub.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>

#include "glog/logging.h"

namespace seven_summits {

// static
template <class Request, class Response>
Stub<Request, Response>* Stub<Request, Response>::CreateFromTcpHostPort(
    const std::string& tcp_host,
    int tcp_port) {
  return new TcpHostPortStub<Request, Response>(tcp_host, tcp_port);
}

// get sockaddr, IPv4 or IPv6:
inline void* get_in_addr(
    struct sockaddr *sa) {
  if (sa->sa_family == AF_INET) {
    return &(((struct sockaddr_in*)sa)->sin_addr);
  }

  return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

template <class Request, class Response>
bool TcpHostPortStub<Request, Response>::Connect() {
  int sockfd, numbytes;
  struct addrinfo hints, *servinfo, *p;
  int rv;
  char s[INET6_ADDRSTRLEN];

  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_protocol = IPPROTO_TCP;

  char port[10];
  sprintf(&port[0], "%d", (int) tcp_port_);
  if ((rv = getaddrinfo(tcp_host_.c_str(), port, &hints, 
                        &servinfo)) != 0) {
    LOG(ERROR) << "getaddrinfo: " << gai_strerror(rv);
    return false;
  }

  // loop through all the results and connect to the first we can
  for(p = servinfo; p != NULL; p = p->ai_next) {
    if ((sockfd = socket(p->ai_family, p->ai_socktype,
                         p->ai_protocol)) == -1) {
      LOG(ERROR) << "client: socket";
      continue;
    }

    int flags = 1;
    int error = setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (void *)&flags,
                           sizeof(flags));
    if (error != 0) {
      LOG(ERROR) << "setsockopt";
      perror("setsockopt");
    }

    if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
      close(sockfd);
      VLOG(1) << "client: connect failed";
      continue;
    }

    break;
  }

  if (p == NULL) {
    LOG(ERROR) << "client: failed to connect\n";
    return false;
  }

  inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
            s, sizeof s);
  VLOG(1) << "client: connecting to " << s;

  freeaddrinfo(servinfo); // all done with this structure
  sfd_ = sockfd;
  return true;
}

template <class Request, class Response>
bool TcpHostPortStub<Request, Response>::Query(
    const Request& request,
    Response* response) {
  uint32_t num_bytes = request.ByteSize();
  ssize_t num_sent = send(sfd_, &num_bytes, sizeof(uint32_t), 0);
  if (num_sent <= 0) {
    LOG(ERROR) << "could not send request";
    return false;
  }
  VLOG(2) << "sent num bytes " << num_bytes;
  if (!request.SerializeToFileDescriptor(sfd_)) {
    LOG(ERROR) << "sending request failed";
    return false;
  }
  if (VLOG_IS_ON(2)) {
    VLOG(2) << "sent request\n" << request.DebugString();
  }
  char buf[kBufSize];
  uint32_t num_response_bytes = 0;
  ssize_t num_recv = read(sfd_, &num_response_bytes, sizeof(uint32_t));
  if (num_recv < 0) {
    VLOG(2) << "could not read response: " << num_recv;
    return false;
  }

  VLOG(2) << "expecting response of length\n" << num_response_bytes;
  num_recv = read(sfd_, (void*) &buf, num_response_bytes);
  if (num_recv < 0) {
    VLOG(0) << "could not read response: " << num_recv;
    return false;
  }

  VLOG(2) << "received response of length\n" << num_recv;
  std::string response_string(buf, num_recv);

  if (!response->ParseFromString(response_string)) {
    VLOG(2) << "parsing response failed";
    return false;
  }
  if (VLOG_IS_ON(2)) {
    VLOG(2) << "received response\n" << response->DebugString();
  }
 
  return true;
}

template <class Request, class Response>
bool TcpHostPortStub<Request, Response>::HangUp() {
  uint32_t num_bytes = 0;
  if (sizeof(uint32_t) != send(sfd_, &num_bytes, sizeof(uint32_t), 0)) {
    LOG(ERROR) << "could not say good-bye";
  }

  int res = close(sfd_);
  if (res != 0) {
    LOG(ERROR) << "close failed";
    perror("close");
  }
  sfd_ = kUndefined;
  return res == 0;
}

}  // namespace seven_summits

#endif // SRC_STUB_INL_H_
